using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Xml;

namespace SSISBatchGeocoder
{
    /// <summary>
    /// This calls encapsulates the logic necessary to geocode a batch of address using Bing Maps's geocoding flow.  The class is designed to be 
    /// independent of SSIS so that users could leverage this code for other, non-SSIS purposes.
    /// </summary>
    public class Geocoder 
    {
        #region Constants
        private const string sBingURL = "https://spatial.virtualearth.net";                             //Bing maps API request URL
        private const string sBingPath = "/REST/v1/Dataflows/Geocode";                                  //Geocode flow path
        private const string sReqXmlNS = "http://schemas.microsoft.com/search/local/2010/5/geocode";    //Geocode request XML namespace

        private const int UPLOAD_CHUNK_SIZE = 100000;               // when uploading files to Bing Maps, used to break down the file into chunks for multiple calls
        private const int JOB_STATUS_POLL_DELAY = 30000;            // delay (ms) when polling the job status
        private const int CURSOR_UNINITIALIZED = -1;                // this value indicates the cursor, used when reading geocoded data, is uninitialized
        private const int CURSOR_EOF = -2;                          // this value indicates the cursor, used when reading geocoded data, has reached the end
        #endregion

        #region Classes
        private class DownloadDetails
        {
            public string jobStatus { get; set; }
            public string suceededlink { get; set; }
            public string failedlink { get; set; }
        }

        private class GeocodeResults
        {
            public string sEntityID;
            public string sEntityType;
            public string sStatusCode;
            public string sConfidence;
            public string sFormattedAddress;
            public string sAddressLine;
            public string sAdminDistrict;
            public string sCountryRegion;
            public string sDistrict;
            public string sLocality;
            public string sPostalCode;
            public string sPostalTown;
            public bool fHasRooftopLocation;
            public double dRooftopLatitude;
            public double dRooftopLongitude;
            public bool fHasInterpolatedLocation;
            public double dInterpolatedLatitude;
            public double dInterpolatedLongitude;
        }
        #endregion Classes

        #region Private Member Variables
        private string sJobDescription = null;                          // Geocode job description string
        private string sBingKey = null;                                 // Bing Maps key
        private MemoryStream dataStream = null;                  // The data source XML stream
        private XmlWriter dataSource = null;                     // The data source XML writer
        private List<GeocodeResults> geocodedRows = null;               // The results from the batch
        private bool useStubCode;                                       // if true, indicates we should fabricate the lat/lon values and avoid a web service call
        private int addressCount;                                       // set to the number of addresses the caller has added by invoking AddAddress
        private GeocodeResults geocodedFields;                          // Geocoded fields from the current row for read operations
        private int cursorPos = CURSOR_UNINITIALIZED;                   // the current position of the read cursor (rows)
        private bool isGeocoded;                                        // set to true once the current batch is geocoded
        #endregion

        // This event is thrown externally whenever a meaningful operation
        // has taken place.  This allows the caller to consume this event
        // and log appropriately according to their configuration.
        public EventHandler<LogVerboseEventArgs> RaiseLogVerboseEvent;

        /// <summary>
        /// Initializes a new Geocoder component to perform batch geocoding. 
        /// </summary>
        /// <param name="jobdescription">The description of the geocode batch.</param>
        /// <param name="bingKey">The password of the Bing Maps account.</param>
        /// <param name="retryAttempts">Number of attempts to retry accessing Bing Maps services in case of network failure.</param>
        /// <param name="retryDelay">The delay between retries (in seconds).</param>
        /// <param name="stubData">If true, will cause the transformation to avoid any Web Service calls and instead will synthesis fake lat/lon values instead of invoking the service. Useful for certain testing scenarios.</param>
        public Geocoder(string jobdescription, string bingKey, int retryAttempts, int retryDelay, bool stubData)
        {
            this.useStubCode = stubData;
            this.sJobDescription = jobdescription;
            this.sBingKey = bingKey;

            // Call our internal Reset() to prepare for usage
            Reset();
        }

        /// <summary>
        /// Adds an Address to be geocoded.  Once the entire list of addresses have been added, call ExecuteBatch to geocode.
        /// <remarks>This overloaded method adds the full set of supported fields for Bing Maps geocoding.</remarks>
        /// </summary>
        /// <param name="Id">The unique identifier for this address</param>
        /// <param name="AddressLine">The address line of the address</param>
        /// <param name="Locality">The city of the address</param>
        /// <param name="AdminDistrict"></param>
        /// <param name="PostalCode">The postalcode of the address</param>
        /// <param name="CountryRegion"></param>
        public void AddAddress(string Id, string Culture, string Query, string AddressLine, string AdminDistrict, string CountryRegion, string District, string Locality, string PostalCode, string PostalTown, string MininumConfidence)
        {
            if (!string.IsNullOrEmpty(Id) &&
                (!string.IsNullOrEmpty(Query) ||
                !string.IsNullOrEmpty(AddressLine) ||
                !string.IsNullOrEmpty(AdminDistrict) ||
                !string.IsNullOrEmpty(CountryRegion) ||
                !string.IsNullOrEmpty(District) ||
                !string.IsNullOrEmpty(Locality) ||
                !string.IsNullOrEmpty(PostalCode) ||
                !string.IsNullOrEmpty(PostalTown)))
            {
                //There must be an entity ID and SOME search key or else do not submit the request
                dataSource.WriteStartElement("GeocodeEntity", sReqXmlNS);
                dataSource.WriteAttributeString("Id", Id);
                dataSource.WriteStartElement("GeocodeRequest", sReqXmlNS);

                if (!string.IsNullOrEmpty(Culture))
                    dataSource.WriteAttributeString("Culture", Culture);

                if (!string.IsNullOrEmpty(Query))
                {
                    //Issue only composite query if defined and not empty
                    dataSource.WriteAttributeString("Query", Query);
                }
                else    
                {
                    //Otherwise use individual search keys as supplied
                    dataSource.WriteStartElement("Address", sReqXmlNS);

                    if (!string.IsNullOrEmpty(AddressLine))
                        dataSource.WriteAttributeString("AddressLine", AddressLine);

                    if (!string.IsNullOrEmpty(AdminDistrict))
                        dataSource.WriteAttributeString("AdminDistrict", AdminDistrict);

                    if (!string.IsNullOrEmpty(CountryRegion))
                        dataSource.WriteAttributeString("CountryRegion", CountryRegion);

                    if (!string.IsNullOrEmpty(District))
                        dataSource.WriteAttributeString("District", District);

                    if (!string.IsNullOrEmpty(Locality))
                        dataSource.WriteAttributeString("Locality", Locality);

                    if (!string.IsNullOrEmpty(PostalCode))
                        dataSource.WriteAttributeString("PostalCode", PostalCode);

                    if (!string.IsNullOrEmpty(PostalTown))
                        dataSource.WriteAttributeString("PostalTown", PostalTown);

                    //Apply minimum confidence filter to address if requested
                    if (!string.IsNullOrEmpty(MininumConfidence))
                    {
                        switch (MininumConfidence)
                        {
                            case "High":
                            case "Medium":
                            case "Low":
                                dataSource.WriteStartElement("ConfidenceFilter", "");
                                dataSource.WriteAttributeString("MinimumConfidence", MininumConfidence);
                                dataSource.WriteEndElement();    //ConfidenceFilter
                                break;
                        }
                    }

                    dataSource.WriteEndElement();    //Address
                }

                dataSource.WriteEndElement();    //GeocodeRequest
                dataSource.WriteEndElement();    //GeocodeEntity

                addressCount++;
            }
        }

        /// <summary>
        /// Geocodes the current batch by orchestrating multiple steps using Bing Maps geocoding flow.  After this method is called, the geocoded values
        /// can be obtained by calling Read() and Get[FIELD].
        /// </summary>
        /// <param name="dataStream">Data to geocode</param>
        /// <param name="dataSourceName"></param>
        /// <param name="entityTypeName"></param>
        /// <returns></returns>
        public void ExecuteBatch()
        {
            if (addressCount == 0) { throw new ApplicationException("Cannot call ExecuteBatch when there are no addresses to geocode."); }

            // Determine if we should stub the geocoding, or invoke the actual service
            //if (!useStubCode)
            //{
                ExecuteBingGeocodeFlow();
            //}
            //else
            //{
            //    ExecuteBatchStubbed();
            //}

            // Initialize some additional variables for reading.
            cursorPos = CURSOR_UNINITIALIZED;
            isGeocoded = true;
        }

        /// <summary>
        /// Geocodes the current batch using the Bing Maps service.
        /// </summary>
        /// <remarks></remarks>
        /// </returns>
        private void ExecuteBingGeocodeFlow()
        {
            dataSource.WriteEndElement();    //Finish the root element
            dataSource.Close();              //Close the XML document and underlying data stream

            string dataflowJobLocation = null;      //The URL of the Bing Maps job
            HttpWebRequest request = null;          //The upload HTTP request

            //Define parameters for the HTTP request
            //
            string contentType = "application/xml; charset=utf-8"; //We will be posting UTF-8 XML data
            string sBingQueryString = null;  //Query string for all Bing HTTP queries

            {
                StringBuilder queryStringBuilder = new StringBuilder();
                
                if (!string.IsNullOrEmpty(this.sJobDescription) && this.sJobDescription.Length != 0)
                    queryStringBuilder.Append("description=").Append(Uri.EscapeUriString(this.sJobDescription));
                
                queryStringBuilder.Append("&");
                queryStringBuilder.Append("input=xml");
                queryStringBuilder.Append("&");
                queryStringBuilder.Append("output=xml");
                queryStringBuilder.Append("&");
                queryStringBuilder.Append("key=" + Uri.EscapeUriString(this.sBingKey));
                sBingQueryString = queryStringBuilder.ToString();
            }

            //Build the HTTP URI that will upload and create the geocode dataflow job
            UriBuilder uriBuilder = new UriBuilder(sBingURL);
            uriBuilder.Path = sBingPath;
            uriBuilder.Query = sBingQueryString;

            //throw new Exception("Not ready to test");

            //Include the data to geocode in the HTTP request
            request = (HttpWebRequest)WebRequest.Create(uriBuilder.Uri);

            //
            // The HTTP method must be 'POST'.
            //
            request.Method = "POST";
            request.ContentType = contentType;

            // Copy the XML data stream to the upload request stream
            // This is necessary because the data stream is completed before initiating the upload request,
            // to minimize the risk of an HTTP request timeout while reading the source data. Any substantial
            // delay to completion of the request results in the connection being forcibly closed.
            using (Stream requestStream = request.GetRequestStream())
            {
                dataStream.Position = 0;
                byte[] localBuffer = new byte[dataStream.Length];
                dataStream.Read(localBuffer, 0, localBuffer.Length);
                requestStream.Write(localBuffer, 0, localBuffer.Length);
                dataStream = null; //Release XML data stream storage
            }

            //Submit the HTTP request and check if the job was created successfully. 
            using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
            {
                //
                // If the job was created successfully, the status code should be
                // 201 (Created) and the 'Location' header should contain a URL
                // that defines the location of the new dataflow job. You use this 
                // URL with the Bing Maps Key to query the status of your job.
                //
                if (response.StatusCode != HttpStatusCode.Created)
                    throw new Exception("An HTTP error status code was encountered while submitting the geocode job: " + (response.StatusDescription != null && response.StatusDescription.Length != 0 ? response.StatusDescription : response.StatusCode.ToString()));

                //string sAuthRC = response.GetResponseHeader("AuthenticationResultCode");

                //if (String.IsNullOrEmpty(dataflowJobLocation))
                //    throw new Exception("The 'AuthenticationResultCode' header is missing from the HTTP response when creating a geocode job.");

                dataflowJobLocation = response.GetResponseHeader("Location");

                if (String.IsNullOrEmpty(dataflowJobLocation))
                    throw new Exception("The 'Location' header was missing from the HTTP response after submitting a geocode job; unable to proceed.");
            }

            DownloadDetails statusDetails = new DownloadDetails();

            do
            {
                statusDetails.jobStatus = "Pending";

                //Build the HTTP Request to get job status
                uriBuilder = new UriBuilder(dataflowJobLocation);
                uriBuilder.Query = sBingQueryString;
                request = (HttpWebRequest)WebRequest.Create(uriBuilder.Uri);

                request.Method = "GET";
                request.ContentType = contentType;

                //Submit the request and read the response to get job status and to retrieve the links for 
                //  downloading the job results
                //Note: The following conditional statements make use of the fact that the 'Status' field will  
                //  always appear after the 'Link' fields in the HTTP response.
                using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                        throw new Exception("An HTTP error status code was encountered while checking the status of the geocode job: " + (response.StatusDescription != null && response.StatusDescription.Length != 0 ? response.StatusDescription : response.StatusCode.ToString()));

                    using (Stream receiveStream = response.GetResponseStream())
                    {
                        XmlTextReader reader = new XmlTextReader(receiveStream);
                        while (reader.Read())
                        {
                            if (reader.IsStartElement())
                            {
                                if (reader.Name.Equals("Status"))
                                {
                                    //return job status
                                    statusDetails.jobStatus = reader.ReadString();

                                    if (statusDetails.jobStatus == "Aborted")
                                    {
                                        string errorMessage = null;

                                        while (reader.Read())
                                        {
                                            if (reader.Name.Equals("ErrorMessage"))
                                            {
                                                errorMessage = reader.ReadString();
                                                break;
                                            }
                                        }

                                        throw new Exception("Job aborted" + (errorMessage == null ? "." : ": " + errorMessage));
                                    }
                                    break;
                                }
                                else if (reader.Name.Equals("Link"))
                                {
                                    //Set the URL location values for retrieving 
                                    // successful and failed job results
                                    reader.MoveToFirstAttribute();

                                    if (reader.Value.Equals("output"))
                                    {
                                        reader.MoveToNextAttribute();

                                        if (reader.Value.Equals("succeeded"))
                                        {
                                            statusDetails.suceededlink = reader.ReadString();

                                        }
                                        else if (reader.Value.Equals("failed"))
                                        {
                                            statusDetails.failedlink = reader.ReadString();
                                        }
                                    }
                                }
                            }
                        }
                    }
                }

                Thread.Sleep(JOB_STATUS_POLL_DELAY); //Get status every 30 seconds
            }
            while (statusDetails.jobStatus.Equals("Pending"));

            //Return the results for data that was geocoded as XML
            if (statusDetails.suceededlink != null && !statusDetails.suceededlink.Equals(String.Empty))
            {
                //Create a request to download successfully geocoded data. You must add the Bing Maps Key to the 
                //  download location URL provided in the response to the job status request.
                UriBuilder successUriBuilder = new UriBuilder(statusDetails.suceededlink);
                successUriBuilder.Query = sBingQueryString;
                HttpWebRequest request1 = (HttpWebRequest)WebRequest.Create(successUriBuilder.Uri);

                request1.Method = "GET";
                request1.ContentType = contentType;

                using (HttpWebResponse response = (HttpWebResponse)request1.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                        throw new Exception("An HTTP error status code was encountered while downloading the geocode good results: " + (response.StatusDescription != null && response.StatusDescription.Length != 0 ? response.StatusDescription : response.StatusCode.ToString()));

                    using (Stream receiveStream = response.GetResponseStream())
                    {
                        //byte[] bdbg = new byte[60000];
                        //int ibr = receiveStream.Read(bdbg, 0, bdbg.Length);
                        //string sdbg = System.Text.Encoding.UTF8.GetString(bdbg, 0, ibr);

                        using (XmlReader r = (XmlReader)XmlReader.Create(receiveStream))
                        {
                            while (r.Read())
                            {
                                if (r.IsStartElement("GeocodeEntity"))
                                {
                                    GeocodeResults result = new GeocodeResults();

                                    if (r.HasAttributes)
                                    {
                                        if (r.MoveToAttribute("Id")) //Read the attributes
                                        {
                                            result.sEntityID = r.Value;
                                        }

                                        r.MoveToElement();
                                    }

                                    if (result.sEntityID == null)
                                    {
                                        continue;
                                    }

                                    if (r.ReadToDescendant("GeocodeResponse", sReqXmlNS)) //Read the response data
                                    {
                                        if (r.HasAttributes)
                                        {
                                            while (r.MoveToNextAttribute())     //Read the attributes
                                            {
                                                switch (r.Name)
                                                {
                                                    case "EntityType":
                                                        result.sEntityType = r.Value;
                                                        break;
                                                    case "Confidence":
                                                        result.sConfidence = r.Value;
                                                        break;
                                                    case "StatusCode":
                                                        result.sStatusCode = r.Value;
                                                        break;
                                                }
                                            }

                                            r.MoveToElement();
                                        }

                                        if (result.sEntityType == null || result.sConfidence == null || result.sStatusCode == null)
                                        {
                                            continue;
                                        }

                                        if (r.ReadToDescendant("Address", sReqXmlNS))
                                        {
                                            if (r.HasAttributes)
                                            {
                                                if (r.MoveToAttribute("AddressLine"))
                                                {
                                                    result.sAddressLine = r.Value;
                                                }

                                                if (r.MoveToAttribute("AdminDistrict"))
                                                {
                                                    result.sAdminDistrict = r.Value;
                                                }

                                                if (r.MoveToAttribute("CountryRegion"))
                                                {
                                                    result.sCountryRegion = r.Value;
                                                }

                                                if (r.MoveToAttribute("District"))
                                                {
                                                    result.sDistrict = r.Value;
                                                }

                                                if (r.MoveToAttribute("FormattedAddress"))
                                                {
                                                    result.sFormattedAddress = r.Value;
                                                }

                                                if (r.MoveToAttribute("Locality"))
                                                {
                                                    result.sLocality = r.Value;
                                                }

                                                if (r.MoveToAttribute("PostalCode"))
                                                {
                                                    result.sPostalCode = r.Value;
                                                }

                                                if (r.MoveToAttribute("PostalTown"))
                                                {
                                                    result.sPostalTown = r.Value;
                                                }

                                                r.MoveToElement();
                                                r.Skip();   //Position on next element
                                            }

                                            while (r.IsStartElement())  //Examine sibling elements at this level
                                            {
                                                if (r.IsStartElement("RooftopLocation", sReqXmlNS))
                                                {
                                                    if (r.HasAttributes)
                                                    {
                                                        if (r.MoveToAttribute("Latitude"))
                                                        {
                                                            if (double.TryParse(r.Value, System.Globalization.NumberStyles.Any, 
                                                                System.Globalization.CultureInfo.InvariantCulture, out result.dRooftopLatitude))
                                                            {
                                                                if (r.MoveToAttribute("Longitude"))
                                                                {
                                                                    if (double.TryParse(r.Value, System.Globalization.NumberStyles.Any, 
                                                                        System.Globalization.CultureInfo.InvariantCulture, out result.dRooftopLongitude))
                                                                    {
                                                                        result.fHasRooftopLocation = true;
                                                                    }
                                                                }
                                                            }
                                                        }

                                                        r.MoveToElement();
                                                    }
                                                }
                                                else if (r.IsStartElement("InterpolatedLocation", sReqXmlNS))
                                                {
                                                    if (r.HasAttributes)
                                                    {
                                                        if (r.MoveToAttribute("Latitude"))
                                                        {
                                                            if (double.TryParse(r.Value, System.Globalization.NumberStyles.Any, 
                                                                System.Globalization.CultureInfo.InvariantCulture, out result.dInterpolatedLatitude))
                                                            {
                                                                if (r.MoveToAttribute("Longitude"))
                                                                {
                                                                    if (double.TryParse(r.Value, System.Globalization.NumberStyles.Any, 
                                                                        System.Globalization.CultureInfo.InvariantCulture, out result.dInterpolatedLongitude))
                                                                    {
                                                                        result.fHasInterpolatedLocation = true;
                                                                    }
                                                                }
                                                            }
                                                        }

                                                        r.MoveToElement();
                                                    }
                                                
                                                }

                                                r.Skip();   //Move past this element
                                            }

                                            if (result.fHasRooftopLocation || result.fHasInterpolatedLocation)
                                                geocodedRows.Add(result);
                                        }
                                    }
                                }
                            }
                        }
                    }

                }
            }

            //Download spatial data that could not be geocoded
            if (statusDetails.failedlink != null && !statusDetails.failedlink.Equals(String.Empty))
            {
                //Create an HTTP request to download error information. You must add the Bing Maps Key to the 
                //  download location URL provided in the response to the job status request.
                UriBuilder failedUriBuilder = new UriBuilder(statusDetails.failedlink);
                failedUriBuilder.Query = sBingQueryString;
                HttpWebRequest request2 = (HttpWebRequest)WebRequest.Create(failedUriBuilder.Uri);

                request2.Method = "GET";
                request2.ContentType = contentType;

                using (HttpWebResponse response = (HttpWebResponse)request2.GetResponse())
                {
                    if (response.StatusCode != HttpStatusCode.OK)
                        throw new Exception("An HTTP error status code was encountered while downloading the geocode bad results: " + (response.StatusDescription != null && response.StatusDescription.Length != 0 ? response.StatusDescription : response.StatusCode.ToString()));

                    using (Stream receiveStream = response.GetResponseStream())
                    {
                        using (XmlReader r = (XmlReader)XmlReader.Create(receiveStream))
                        {
                            while (r.Read())
                            {
                                if (r.IsStartElement("GeocodeEntity"))
                                {
                                    GeocodeResults result = new GeocodeResults();

                                    if (r.HasAttributes)
                                    {
                                        if (r.MoveToAttribute("Id")) //Read the attributes
                                        {
                                            result.sEntityID = r.Value;
                                        }

                                        r.MoveToElement();
                                    }

                                    if (result.sEntityID == null)
                                    {
                                        continue;
                                    }

                                    if (r.ReadToDescendant("GeocodeResponse")) //Read the response data
                                    {
                                        if (r.HasAttributes)
                                        {
                                            while (r.MoveToNextAttribute())     //Read the attributes
                                            {
                                                switch (r.Name)
                                                {
                                                    case "StatusCode":
                                                        result.sStatusCode = r.Value;
                                                        break;
                                                    case "FaultReason":
                                                        result.sConfidence = r.Value;
                                                        break;
                                                }
                                            }

                                            r.MoveToElement();
                                        }

                                        geocodedRows.Add(result);                    
                                    }
                                }
                            }
                        }
                    }
                }
            }

            return;
        }

        ///// <summary>
        ///// Fakes the geocoding of the current batch using random numbers for lat/lon.
        ///// </summary>
        ///// <returns></returns>
        //private string ExecuteBatchStubbed()
        //{
        //    OnLogVerbose("Generating stubbed geocoding data (Bing Maps Services will not be called.  Please check the 'Service Stub Bing Maps Data' option for more info).");

        //    // Convert the stream into a text block
        //    byte[] localBuffer = new byte[dataStream.Length];
        //    dataStream.Position = 0;
        //    dataStream.Read(localBuffer, 0, (int)dataStream.Length);
        //    string data = System.Text.Encoding.Default.GetString(localBuffer);

        //    // Prepare the variables used to randomize the geocoding values
        //    Random rnd = new Random();
        //    StringBuilder outputFile = new StringBuilder(data.Length);
        //    string[] sampleMatchCodes = { "Good", "Good", "Good", "Good", "Bad", "Imprecise", "Ambiguous" };      // give 4 to 1 of Good to other values to make more realistic

        //    // Break the batch down into rows
        //    string[] rows = data.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);

        //    // Prepare the output buffer by adding the header
        //    outputFile.Append("<encoding name='UTF-8'>\r\n");     // append XML header declaration
        //    outputFile.Append(rows[0] + "\r\n");     // then append header

        //    // For each row in the batch
        //    for (int iRow = 1; iRow < rows.Length; iRow++ )     // skip the header
        //    {
        //        string[] cols = rows[iRow].Split(new string[] { "," }, StringSplitOptions.None);

        //        // Add the original values back to the output
        //        outputFile.Append(cols[0] + ",");
        //        outputFile.Append(cols[1] + ",");
        //        outputFile.Append(cols[2] + ",");
        //        outputFile.Append(cols[3] + ",");
        //        outputFile.Append(cols[4] + ",");
        //        outputFile.Append(cols[5] + ",");
        //        outputFile.Append(cols[6] + ",");

        //        // Now add the randomized geocoded values (including match code)
        //        outputFile.Append((rnd.NextDouble() * 90).ToString() + ",");
        //        outputFile.Append((rnd.NextDouble() * 180).ToString() + ",");       // ignores possibility of negative longitude, but good enough for testing
        //        outputFile.Append(sampleMatchCodes[rnd.Next(0, 6)] + "\r\n");
        //    }

        //    return outputFile.ToString();
        //}

        /// <summary>
        /// Reads the next row of geocoded data.  Use the Get[FIELD] properties to retrieve the geocoded columns of data.
        /// <remarks>This method can only be called after ExecuteBatch().</remarks>
        /// </summary>
        /// <returns>True if another row is available, or false if the EOF is reached.</returns>
        public bool Read()
        {
            // Verify we have another row to read
            if ((cursorPos + 1) >= geocodedRows.Count || cursorPos == CURSOR_EOF)
            {
                // We do not, so close the cursor
                cursorPos = CURSOR_EOF;
                return false;
            }
            else if (!isGeocoded)
            {
                throw new ApplicationException("Cannot call Read before ExecuteBatch has been invoked.");
            }

            cursorPos++;        // advance the cursor (starts at -1)

            // Read the fields of the current row
            geocodedFields = geocodedRows[cursorPos];
            return true;
        }

        /// <summary>
        /// Prepares the Geocoder for another batch by resetting the component's state.
        /// </summary>
        public void Reset()
        {
            this.isGeocoded = false;
            this.cursorPos = CURSOR_UNINITIALIZED;
            this.geocodedRows = null;
            this.geocodedFields = null;
            this.addressCount = 0;
            this.dataSource = null;
            this.dataStream = null;
            this.dataStream = new MemoryStream();                    //Create the stream to which the request XML is written
            this.dataSource = XmlWriter.Create(dataStream);   //Create the XML geocode request (default UTF-8 encoding)
            this.dataSource.WriteStartElement("GeocodeFeed");        //Start root element of request
            this.geocodedRows = new List<GeocodeResults>();                 //Initialize the results collection
        }

        /// <summary>The size of the current batch stream in bytes.</summary>
        public long BatchSize
        { get { return dataStream.Length; } }

        /// <summary>The number of addresses added to to the batch.</summary>
        public int AddressCount
        { get { return addressCount; } }

        /// <summary>The EntityID value returned from Bing Maps.</summary>
        public string GetEntityID
        { get { return geocodedFields.sEntityID; } }

        /// <summary>The EntityType value returned from Bing Maps.</summary>
        public string GetEntityType
        { get { return geocodedFields.sEntityType; } }

        /// <summary>The HasRooftopLocation value.</summary>
        public bool GetHasRooftopLocation
        { get { return geocodedFields.fHasRooftopLocation; } }

        /// <summary>The Latitude value returned from Bing Maps.</summary>
        public double GetRooftopLatitude
        { get { return geocodedFields.dRooftopLatitude; } }

        /// <summary>The Longitude value returned from Bing Maps.</summary>
        public double GetRooftopLongitude
        { get { return geocodedFields.dRooftopLongitude; } }

        /// <summary>The HasInterpolatedLocation value.</summary>
        public bool GetHasInterpolatedLocation
        { get { return geocodedFields.fHasInterpolatedLocation; } }

        /// <summary>The Latitude value returned from Bing Maps.</summary>
        public double GetInterpolatedLatitude
        { get { return geocodedFields.dInterpolatedLatitude; } }

        /// <summary>The Longitude value returned from Bing Maps.</summary>
        public double GetInterpolatedLongitude
        { get { return geocodedFields.dInterpolatedLongitude; } }

        /// <summary>The StatusCode value returned from Bing Maps.</summary>
        public string GetStatusCode
        { get { return geocodedFields.sStatusCode; } }

        /// <summary>The Confidence value returned from Bing Maps.</summary>
        public string GetConfidence
        { get { return geocodedFields.sConfidence; } }

        /// <summary>The FormattedAddress value returned from Bing Maps.</summary>
        public string GetFormattedAddress
        { get { return geocodedFields.sFormattedAddress; } }

        public string GetAddressLine
        { get { return geocodedFields.sAddressLine; } }

        public string GetAdminDistrict
        { get { return geocodedFields.sAdminDistrict; } }

        public string GetCountryRegion
        { get { return geocodedFields.sCountryRegion; } }

        public string GetDistrict
        { get { return geocodedFields.sDistrict; } }

        public string GetLocality
        { get { return geocodedFields.sLocality; } }

        public string GetPostalCode
        { get { return geocodedFields.sPostalCode; } }

        public string GetPostalTown
        { get { return geocodedFields.sPostalTown; } }

        /// <summary>
        /// Called internally when any useful event is encountered. Will throw a LogVerbose event to the caller for logging purposes.
        /// </summary>
        /// <param name="message">A description of the event.</param>
        private void OnLogVerbose(string message)
        {
            // Event will be null if there are no subscribers
            if (RaiseLogVerboseEvent != null)
            {
                LogVerboseEventArgs args = new LogVerboseEventArgs(message);
                RaiseLogVerboseEvent(this, args);
            }
        }
    }

    public class LogVerboseEventArgs : EventArgs
    {
        private string msg;
        public LogVerboseEventArgs(string message) { msg = message; }
        public string Message { get { return msg; } }
    }
}